Saltar al contenido principal

5.4 Funciones de orden superior

En Python, las funciones son ciudadanos de primera clase (first-class objects), lo que significa que se pueden almacenar en variables, pasar como argumentos y devolver como resultado de otras funciones. Este comportamiento permite crear las llamadas funciones de orden superior, es decir, funciones que reciben otras funciones como parámetro o las devuelven como resultado.

Este concepto procede de la programación funcional y ofrece una gran flexibilidad a la hora de estructurar el código.


1️⃣ Qué es una función de orden superior​

Una función de orden superior es aquella que:

  1. Recibe una función como argumento, o
  2. Devuelve una función como resultado, o ambas cosas a la vez.

Gracias a este mecanismo, Python permite construir estructuras más expresivas y reutilizables.

Ejemplo de función que recibe otra función
def aplicar_operacion(funcion, valor):
"""Aplica la función recibida al valor dado."""
return funcion(valor)

def cuadrado(x):
return x ** 2

print(aplicar_operacion(cuadrado, 5))
en este ejemplo
  • La función aplicar_operacion() recibe como argumento otra función (cuadrado) y un valor (5).
  • Dentro de aplicar_operacion() se ejecuta funcion(valor), que en este caso equivale a cuadrado(5), devolviendo 25.
  • De este modo, una función puede actuar sobre otras funciones sin conocer su implementación concreta.

2️⃣ Funciones como objetos de primera clase​

En Python, las funciones son tratadas como cualquier otro tipo de dato. Esto significa que se pueden:

  • Asignar a variables,
  • Pasar como parámetros,
  • Guardar en estructuras de datos,
  • Y devolver como resultado de otra función.
Ejemplo, almacenamiento de funciones en una lista
def sumar(a, b): 
return a + b

def restar(a, b):
return a - b

def multiplicar(a, b):
return a * b

operaciones = [sumar, restar, multiplicar]

for operacion in operaciones:
print(operacion(5, 3))
en este ejemplo
  • Cada elemento de la lista operaciones es una referencia a una función.
  • El bucle for recorre la lista y ejecuta cada una, mostrando los resultados: 8, 2 y 15.
  • Esta técnica resulta útil para aplicar un conjunto de operaciones de forma dinámica.

3️⃣ Funciones que devuelven otras funciones​

Una función de orden superior también puede devolver otra función. Esto se utiliza, por ejemplo, para crear funciones personalizadas o configuradas dinámicamente.

Ejemplo, función que genera otras funciones
def crear_multiplicador(factor):
"""Devuelve una nueva función que multiplica por el factor dado."""
def multiplicar(numero):
return numero * factor
return multiplicar

por_dos = crear_multiplicador(2)
por_tres = crear_multiplicador(3)

print(por_dos(10))
print(por_tres(10))
en este ejemplo
  • La función crear_multiplicador() devuelve otra función (multiplicar) que recuerda el valor de factor.
  • Cuando se llama a por_dos(10), devuelve 20, y por_tres(10) devuelve 30.

Este comportamiento se conoce como closure (clausura), y permite mantener valores en memoria incluso después de que la función exterior haya finalizado.


4️⃣ Funciones de orden superior incorporadas​

Python incluye varias funciones de orden superior muy utilizadas que ya hemos presentado en el punto anterior: map(), filter() y reduce(). Estas funciones toman otra función como argumento para aplicar transformaciones o filtrados sobre colecciones de datos.

🟩 map()​

map(funcion, iterable) aplica la función especificada a cada elemento del iterable y devuelve un nuevo objeto de tipo map.

Uso de map() con una función normal
def elevar_cuadrado(x):
return x ** 2

numeros = [1, 2, 3, 4, 5]
resultado = list(map(elevar_cuadrado, numeros))
print(resultado)
en este ejemplo
  • Cada elemento de la lista numeros se eleva al cuadrado mediante la función elevar_cuadrado.
  • El resultado final es [1, 4, 9, 16, 25].

Esta misma operación podría realizarse con una función lambda para simplificar aún más el código.

🟦 filter()​

filter(funcion, iterable) recorre una colección y conserva solo los elementos que hacen que la función devuelva True.

Uso de filter() para seleccionar números pares
def es_par(n):
return n % 2 == 0

numeros = [10, 15, 20, 25, 30]
pares = list(filter(es_par, numeros))
print(pares)
en este ejemplo
  • La función es_par() devuelve True solo para los números pares.
  • filter() aplica esa función a cada elemento y devuelve una nueva lista con los que cumplen la condición: [10, 20, 30].

🟨 reduce()​

reduce(funcion, iterable) aplica una función acumulativa a los elementos de una secuencia. A diferencia de map() y filter(), no está incorporada directamente, sino que pertenece al módulo functools.

Uso de reduce() para calcular un producto total
from functools import reduce

def multiplicar(x, y):
return x * y

numeros = [1, 2, 3, 4, 5]
resultado = reduce(multiplicar, numeros)
print(resultado)
en este ejemplo
  • reduce() aplica la función multiplicar acumulativamente sobre la lista.
  • Primero multiplica 1×2, luego el resultado por 3, y así sucesivamente, hasta obtener 120.

Es una herramienta poderosa para combinar elementos en un único resultado.


5️⃣ Creación de funciones personalizadas con closures​

Los closures son funciones internas que conservan el estado de las variables de la función exterior incluso después de que esta haya terminado. Se utilizan para crear funciones configuradas que recuerdan un valor o contexto determinado.

Ejemplo de closure
def contador():
"""Devuelve una función que incrementa un contador interno."""
n = 0
def incrementar():
nonlocal n
n += 1
return n
return incrementar

c = contador()
print(c()) # 1
print(c()) # 2
print(c()) # 3
en este ejemplo
  • La variable n pertenece al ámbito de contador() y se conserva gracias al uso de nonlocal.
  • Cada vez que se llama a c(), el valor interno de n se incrementa, manteniéndose entre ejecuciones.

Esto demuestra que las funciones pueden retener un “estado interno” sin usar variables globales.


6️⃣ Ejemplos prácticos de funciones de orden superior​

🟩 Ejemplo 1: transformación de datos​

Aplicar una función a todos los elementos de una lista
def aplicar(funcion, datos):
"""Aplica una función a cada elemento de una lista."""
return [funcion(x) for x in datos]

numeros = [1, 2, 3, 4]
resultado = aplicar(lambda x: x ** 3, numeros)
print(resultado)
en este ejemplo
  • La función aplicar() recibe otra función (lambda x: x ** 3) y una lista de números.
  • Devuelve una nueva lista con cada número elevado al cubo.

🟦 Ejemplo 2: filtrado condicional​

Filtrar elementos según una condición
def filtrar(funcion, datos):
"""Devuelve una lista con los elementos que cumplen la condición."""
return [x for x in datos if funcion(x)]

valores = [5, 8, 13, 21, 34]
pares = filtrar(lambda x: x % 2 == 0, valores)
print(pares)
en este ejemplo
  • La función filtrar() recibe como argumento una función que comprueba si un número es par.
  • El resultado será [8, 34].

🟨 Ejemplo 3: devolver funciones configuradas​

Generar funciones configurables
def potencia(n):
"""Devuelve una función que eleva al exponente n."""
def elevar(x):
return x ** n
return elevar

al_cuadrado = potencia(2)
al_cubo = potencia(3)

print(al_cuadrado(5)) # 25
print(al_cubo(2)) # 8
en este ejemplo
  • potencia() genera nuevas funciones personalizadas: una que eleva al cuadrado y otra al cubo.
  • Cada una conserva el valor de n gracias al mecanismo de cierre (closure).

7️⃣ Beneficios de las funciones de orden superior​

Ventajas principales:

  • Permiten mayor modularidad: el comportamiento se pasa como parámetro.
  • Facilitan la reutilización del código.
  • Favorecen un estilo de programación más declarativo y expresivo.
  • Reducen la necesidad de bucles explícitos.

Ejemplo :

Ejemplo con map(), filter() y reduce()
from functools import reduce

numeros = [1, 2, 3, 4, 5]

resultado = reduce(
lambda x, y: x + y,
filter(lambda n: n % 2 == 0, map(lambda n: n ** 2, numeros))
)

print(resultado)
en este ejemplo
  1. map(lambda n: n ** 2, numeros) genera los cuadrados → [1, 4, 9, 16, 25].
  2. filter(lambda n: n % 2 == 0, ...) conserva solo los pares → [4, 16].
  3. reduce(lambda x, y: x + y, ...) suma los valores → 20. Este ejemplo combina tres funciones de orden superior en una única línea clara y compacta.

8️⃣ Decoradores​

Los decoradores son una de las aplicaciones más potentes y elegantes de las funciones de orden superior en Python. Un decorador es una función que recibe otra función como argumento y devuelve una nueva función, generalmente modificando o ampliando su comportamiento sin alterar su código original.

Se utilizan con frecuencia para:

  • Registrar o medir la ejecución de funciones.
  • Controlar el acceso a recursos.
  • Validar parámetros.
  • Implementar patrones como logging, caching o autenticación.

🟩 Concepto básico​

En esencia, un decorador es una función que envuelve otra función. Esto se conoce como el patrón de envoltura (wrapper pattern).

Ejemplo básico de decorador manual
def decorador(funcion):
"""Ejemplo de decorador que muestra mensajes antes y después."""
def envoltura():
print("Antes de ejecutar la función...")
funcion()
print("Después de ejecutar la función...")
return envoltura

def saludo():
print("¡Hola, mundo!")

saludo = decorador(saludo)
saludo()

# Antes de ejecutar la función...
# ¡Hola, mundo!
# Después de ejecutar la función...
en este ejemplo
  1. decorador() recibe la función saludo como argumento.
  2. Dentro de decorador() se define envoltura(), que añade mensajes antes y después de llamar a la función original.
  3. Finalmente, decorador() devuelve la función envoltura().
  4. Al reasignar saludo = decorador(saludo), la llamada saludo() ya no ejecuta directamente la función original, sino la versión decorada.

🟦 Uso de la sintaxis @decorador​

Python ofrece una sintaxis simplificada para aplicar decoradores mediante el símbolo @. En lugar de reasignar manualmente la función, se coloca @nombre_del_decorador justo encima de la definición.

Uso de la sintaxis @decorador
def decorador(funcion):
def envoltura():
print("Ejecutando decorador...")
funcion()
print("Fin de la ejecución.")
return envoltura

@decorador
def saludo():
print("Hola desde la función decorada.")

saludo()
en este ejemplo

El decorador @decorador equivale a escribir saludo = decorador(saludo) justo después de definir la función. Esta forma es más legible y habitual en el código Python moderno.

🟨 Decoradores con argumentos​

Si la función original recibe argumentos, el decorador debe estar preparado para capturarlos y reenviarlos correctamente mediante *args y **kwargs.

Decorador que admite parámetros en la función decorada
def decorador(funcion):
def envoltura(*args, **kwargs):
print("Llamando a la función con argumentos:", args, kwargs)
resultado = funcion(*args, **kwargs)
print("Ejecución finalizada.")
return resultado
return envoltura

@decorador
def sumar(a, b):
print(f"La suma es: {a + b}")
return a + b

resultado = sumar(3, 5)
print("Resultado devuelto:", resultado)
en este ejemplo
  • El decorador ahora puede interceptar los parámetros con *args y **kwargs, imprimirlos o modificarlos, y finalmente pasarlos a la función original.
  • El decorador también puede devolver el resultado original (resultado), lo que lo hace completamente transparente para quien use la función.

🟧 Decoradores anidados​

Una función puede tener varios decoradores aplicados a la vez. En ese caso, se ejecutan en orden ascendente desde el más cercano a la definición de la función original.

Ejemplo de varios decoradores
def mayusculas(funcion):
def envoltura():
return funcion().upper()
return envoltura

def signos_exclamacion(funcion):
def envoltura():
return f"¡{funcion()}!"
return envoltura

@signos_exclamacion
@mayusculas
def saludo():
return "hola mundo"

print(saludo()) # ¡HOLA MUNDO!
en este ejemplo

Primero se aplica @mayusculas, y luego @signos_exclamacion.

Cada decorador modifica el resultado del anterior, formando una cadena de transformaciones.

🟥 Decoradores con argumentos propios​

A veces el decorador en sí necesita recibir parámetros. En estos casos, se define una función externa que recibe los parámetros y devuelve el decorador real.

Decorador con argumentos propios
def repetir(n):
"""Ejecuta la función decorada n veces."""
def decorador(funcion):
def envoltura(*args, **kwargs):
for i in range(n):
print(f"Ejecución {i + 1} de {n}")
funcion(*args, **kwargs)
return envoltura
return decorador

@repetir(3)
def saludo():
print("¡Hola!")

saludo()

# Ejecución 1 de 3
# ¡Hola!
# Ejecución 2 de 3
# ¡Hola!
# Ejecución 3 de 3
# ¡Hola!
en este ejemplo
  1. repetir(3) genera un decorador configurado con n = 3.
  2. Este decorador devuelve la función envoltura(), que llama tres veces a la función original.

🟪 Uso de functools.wraps​

Al aplicar decoradores, Python reemplaza internamente la función original por la función envoltura(). Esto hace que se pierda información como el nombre, el docstring o las anotaciones. Para conservar esos metadatos, se utiliza el decorador @wraps del módulo functools.

Conservación de metadatos con functools.wraps
from functools import wraps

def decorador(funcion):
@wraps(funcion)
def envoltura(*args, **kwargs):
print("Ejecutando:", funcion.__name__)
return funcion(*args, **kwargs)
return envoltura

@decorador
def saludar():
"""Muestra un saludo por pantalla."""
print("Hola a todos")

saludar()
print(saludar.__name__)
print(saludar.__doc__)

# Ejecutando: saludar
# Hola a todos
# saludar
# Muestra un saludo por pantalla.
en este ejemplo

El decorador @wraps(funcion) copia el nombre, docstring y otras propiedades de la función original a la versión decorada.

Esto es una buena práctica recomendada siempre que se definan decoradores personalizados.

🟫 Casos de uso comunes​

Los decoradores se utilizan con frecuencia en aplicaciones reales para tareas repetitivas y transversales, como:

  1. Medición de tiempo de ejecución
import time

def medir_tiempo(funcion):
def envoltura(*args, **kwargs):
inicio = time.time()
resultado = funcion(*args, **kwargs)
fin = time.time()
print(f"Tiempo de ejecución: {fin - inicio:.4f} segundos")
return resultado
return envoltura

@medir_tiempo
def tarea_pesada():
sum([i**2 for i in range(1_000_000)])

tarea_pesada()
  1. Control de acceso
def requiere_autorizacion(funcion):
def envoltura(usuario):
if usuario != "admin":
print("Acceso denegado.")
return
return funcion(usuario)
return envoltura

@requiere_autorizacion
def panel_administrador(usuario):
print(f"Bienvenido, {usuario}.")

panel_administrador("pepe")
panel_administrador("admin")
  1. Registro de llamadas
def registrar(funcion):
def envoltura(*args, **kwargs):
print(f"Llamando a {funcion.__name__} con argumentos {args} {kwargs}")
return funcion(*args, **kwargs)
return envoltura

@registrar
def sumar(a, b):
return a + b

sumar(5, 8)

9️⃣ Generadores​

Los generadores son un tipo especial de función que permite producir una secuencia de valores de forma progresiva, en lugar de devolverlos todos de una vez. A diferencia de las funciones tradicionales, que utilizan return, las funciones generadoras emplean la palabra clave yield para devolver un valor y pausar la ejecución hasta la siguiente llamada.

Cada vez que se invoca el generador, este retoma su ejecución desde el punto en que se detuvo, recordando el estado de sus variables internas.

Los generadores tienen múltiples ventajas...

  1. Eficiencia en memoria.
  2. Ejecución diferida (lazy evaluation).
  3. Código más simple que clases iteradoras.
  4. Apto para flujos infinitos o muy grandes.

🟩 Concepto general​

Un generador se define igual que una función normal, pero incluye al menos una instrucción yield. Cuando se llama a la función, no se ejecuta de inmediato, sino que devuelve un objeto generador, que puede recorrerse con un bucle o con la función next().

Ejemplo básico de generador
def contar_hasta(n):
"""Generador que produce números del 1 al n."""
contador = 1
while contador <= n:
yield contador
contador += 1

for numero in contar_hasta(5):
print(numero)

# 1
# 2
# 3
# 4
# 5
en este ejemplo
  • Cada vez que se ejecuta yield contador, la función devuelve ese valor y se detiene temporalmente.
  • Cuando el bucle for solicita el siguiente valor, la ejecución se reanuda desde la línea siguiente al yield.
  • El proceso continúa hasta que se alcanza el final de la función.

🟦 Diferencias entre return y yield​

Característicareturnyield
Finaliza la funciónSí, termina la ejecuciónNo, la pausa temporalmente
DevuelveUn único valorMúltiples valores, uno a uno
Guarda el estado internoNoSí
Se utiliza conFunciones tradicionalesGeneradores
Tipo de resultadoValor concretoObjeto generador

Ejemplo comparativo:

Comparación entre return y yield
def con_return(n):
resultado = []
for i in range(1, n + 1):
resultado.append(i)
return resultado

def con_yield(n):
for i in range(1, n + 1):
yield i

print(con_return(5))
print(list(con_yield(5)))
en este ejemplo
  • La función con_return() devuelve toda la lista completa [1, 2, 3, 4, 5], ocupando memoria proporcional al tamaño.
  • En cambio, con_yield() genera cada número bajo demanda, lo que resulta mucho más eficiente cuando se trabaja con grandes volúmenes de datos.

🟨 Uso de next() con generadores​

Los generadores son objetos iterables, por lo que pueden recorrerse manualmente con la función next(). Cada llamada a next() produce el siguiente valor. Cuando no quedan más, se lanza la excepción StopIteration.

Uso manual de un generador
g = contar_hasta(3)
print(next(g)) # 1
print(next(g)) # 2
print(next(g)) # 3
# print(next(g)) -> StopIteration
en este ejemplo

Cada llamada a next(g) reanuda la ejecución del generador hasta el siguiente yield. Al completarse, se lanza la excepción StopIteration para indicar que ya no hay más valores.

🟧 Generadores por comprensión​

Además de las funciones con yield, Python ofrece generadores por comprensión, similares a las comprensiones de listas, pero con paréntesis en lugar de corchetes. Estos generadores son aún más compactos y se utilizan para construir iteradores ligeros.

Generador por comprensión
cuadrados = (x ** 2 for x in range(5))
print(cuadrados)
for c in cuadrados:
print(c)
en este ejemplo
  • (x ** 2 for x in range(5)) crea un generador que calcula los cuadrados de los números del 0 al 4.
  • No genera una lista en memoria, sino que produce los valores uno a uno cuando se solicitan.
  • Este enfoque es muy eficiente en operaciones de cálculo o procesamiento de datos secuenciales.

🟥 Uso de yield from​

La instrucción yield from permite delegar parte de la generación a otro iterable o generador, simplificando la sintaxis.

Uso de yield from para delegar la generación
def generador_principal():
yield from range(3)
yield from ["a", "b", "c"]

for valor in generador_principal():
print(valor)

# 0
# 1
# 2
# a
# b
# c
en este ejemplo

yield from delega la producción de valores a otros iterables, evitando escribir bucles adicionales.

🟪 Generadores avanzados send() y close()​

Además de producir valores, los generadores pueden recibir información externa y finalizar de forma controlada.

Estos mecanismos los convierten en pequeñas corutinas.

send(valor): enviar datos al generador​

El método send() permite enviar un valor al punto donde el generador está pausado, sustituyendo el valor que devolvería yield.

Ejemplo básico de send()
def cuenta_atras(tiempo):
"""Generador que hace una cuenta atrás y permite modificar el tiempo."""
restante = tiempo
while restante > 0:
ajuste = yield restante # yield devuelve el tiempo actual
if ajuste is not None: # Si se usa send(), el valor llega aquí
restante += ajuste # buff (+) o debuff (-)
restante -= 1 # avanza un segundo (simulado)

g = cuenta_atras(5)

print(next(g)) # 5
print(next(g)) # 4
print(g.send(+3)) # buff: ahora queda 6
print(next(g)) # 5
print(g.send(-2)) # debuff: ahora queda 2
print(next(g)) # 1
print(next(g)) # 0 → se detendrá después
en este ejemplo
  • El generador empieza devolviendo el tiempo inicial y se detiene en la instrucción yield.
  • Cada llamada a next() continúa la cuenta atrás, reduciendo el contador en 1.
  • Cuando se usa ´, ese valor sustituye al resultado del yield y se recibe dentro del generador como ajuste.
  • Si ajuste es positivo, aumenta el tiempo restante (buff); si es negativo, lo reduce (debuff).
  • La cuenta atrás continúa hasta que restante llega a 0, momento en el que el generador finaliza.

close(): cierre controlado del generador​

El método close() fuerza al generador a terminar inmediatamente. Provoca internamente la excepción GeneratorExit, que puede usarse para limpieza.

Uso de close()
def generador():
try:
while True:
yield "activo"
except GeneratorExit:
print("Cerrando generador...")

g = generador()
print(next(g)) # activo
g.close() # Cierra el generador

🟫 Ejemplo integrado​

Generador que usa send() y close()
def contador():
print("Iniciando generador...")
n = 0
try:
while True:
recibido = yield n
if recibido == "reset":
n = 0
elif isinstance(recibido, int):
n += recibido
except GeneratorExit:
print("Generador cerrado correctamente.")

g = contador()
print(next(g)) # 0
print(g.send(3)) # 3
print(g.send(10)) # 13
print(g.send("reset"))# 0
g.close() # Cierra generador

📊 Tabla resumen​

ConceptoDescripciónEjemplo
Función de orden superiorRecibe o devuelve otra funciónmap(funcion, lista)
Función como datoPuede almacenarse en variables o estructurasoperaciones = [sumar, restar]
map()Aplica una función a todos los elementosmap(lambda x: x*2, lista)
filter()Filtra elementos según una condiciónfilter(lambda x:x>0, lista)
reduce()Acumula valores de una secuenciareduce(lambda x,y:x+y, lista)
ClosureFunción interna que conserva el estado de variables externascontador()
DecoradorFunción que recibe y devuelve otra función@decorador
Función internaAñade nueva lógica a la función originaldef envoltura():
Decoradores anidadosVarios decoradores aplicados a la vez@a encima de @b
Decoradores con argumentosConfigurables mediante funciones externas@repetir(3)
functools.wrapsConserva metadatos de la función original@wraps(funcion)
GeneradorFunción que devuelve una secuencia de valoresyield valor
Objeto generadorResultado de llamar a una función con yieldg = generador()
yieldDevuelve un valor y pausa la ejecucióndentro de un bucle
next()Obtiene el siguiente valor del generadornext(g)
Generador por comprensiónSintaxis abreviada con paréntesis(x**2 for x in range(5))
yield fromDelegación de otro iterableyield from lista
send()Envía un valor al generador y reanuda su ejecucióng.send(valor)
close()Finaliza el generador de forma controladag.close()

Las funciones de orden superior son una herramienta fundamental en Python para escribir código flexible, expresivo y reutilizable. Permiten tratar las funciones como valores, lo que amplía enormemente las posibilidades del lenguaje.

Los generadores son una extensión natural del concepto de función. Permiten trabajar con secuencias grandes o infinitas de manera eficiente, conservando la simplicidad y expresividad característica de Python.